/***********************************************
 This file is part of redia-library-app-ios.
 
 Copyright (c) 2012, 2013 Redia A/S
 
 Redia-library-app-ios is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
 
 Redia-library-app-ios is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with redia-library-app-ios.  If not, see <http://www.gnu.org/licenses/>.
 
 *********************************************** */


#import "UrlCacheManager.h"
#import "defines.h"
#import "UrlCacheEntry.h"

@implementation UrlCacheManager

static UrlCacheManager *sharedSingleton;

@synthesize databasePath, cacheFilesPath;
@synthesize isOffLineMode;
@synthesize cacheFileExpireTimeSeconds;

+ (void)initialize
{
    static BOOL initialized = NO;
    if(!initialized)
    {
        initialized = YES;
        sharedSingleton = [[UrlCacheManager alloc] init];
        sharedSingleton->cacheDB=nil;
		sharedSingleton->cacheFileExpireTimeSeconds = 5*24*60*60; // 5 days
    }
}

+ (UrlCacheManager*)instance
{
	return sharedSingleton;
}

- (id)init
{
    self = [super init];
    if (self) {
		isOffLineMode = NO;
		
		[self setupDB];
    }
    return self;
}

- (void)setupPath
{
	NSString *docsDir;
	NSArray *dirPaths;
	
	// Get the documents directory
	dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	
	docsDir = [dirPaths objectAtIndex:0];
	// Build the path to the database file
	databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: @"cache-v02.db"]];
    
}

- (void)setupDB 
{
	[self setupPath];
	
	NSFileManager *filemgr = [[NSFileManager alloc] init];
	
	if ([filemgr fileExistsAtPath: databasePath ] == NO)
	{
		const char *dbpath = [databasePath UTF8String];
		
		if (sqlite3_open(dbpath, &cacheDB) == SQLITE_OK)
		{
			char *errMsg;
			const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CACHE (ID INTEGER PRIMARY KEY AUTOINCREMENT, URL TEXT, FILENAME TEXT, FILETYPE TEXT, LASTMODIFIED TEXT, WIDTH TEXT, HEIGHT TEXT); CREATE INDEX IF NOT EXISTS urlidx ON cache(url);";
			
			if (sqlite3_exec(cacheDB, sql_stmt, NULL, NULL, &errMsg) != SQLITE_OK)
			{
				NSLog(@"Failed to create table or index: %s",errMsg);
			} 
			sqlite3_close(cacheDB);
			
		} else {
			NSLog(@"Failed to open/create database");
		}
	}
	
	NSArray* cacheDirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
	cacheFilesPath = [cacheDirs objectAtIndex:0];
	cacheFilesPath = [[NSString alloc] initWithString: [cacheFilesPath stringByAppendingPathComponent:@"cachefiles"]];
	if ([filemgr fileExistsAtPath:cacheFilesPath]==NO) {
		[filemgr createDirectoryAtPath:cacheFilesPath withIntermediateDirectories:YES attributes:nil error:nil];
	}
	DLog(@"cache files path is %@",cacheFilesPath);
	
	
		
}

- (void) preloadCacheWithDBFile:(NSURL*)cachefile filesBundle:(NSBundle*)files_bundle
{
    //somehow we must check if the cache is already filled
}

- (bool)openCacheDb
{
    const char *dbpath = [databasePath UTF8String];
	
	if (sqlite3_open(dbpath, &cacheDB) == SQLITE_OK)
	{
        return true;
    } else {
        NSLog(@"ERROR: COULDN'T OPEN CACHE DB");
    }
    return false;
}

- (void)closeCacheDb
{
    sqlite3_close(cacheDB);
    cacheDB=nil;
}

- (void) saveImage:(NSData*)imageData fromUrl:(NSURL*)url imageWidth:(int)imageWidth imageHeight:(int)imageHeight
{
	sqlite3_stmt    *statement;
	sqlite3_stmt    *statement2;
	
	const char *dbpath = [databasePath UTF8String];
	
	if (sqlite3_open(dbpath, &cacheDB) == SQLITE_OK)
	{
		NSDate* now = [NSDate date];
		
		NSString *insertSQL = [NSString stringWithFormat: @"INSERT INTO CACHE (url, filetype, LASTMODIFIED, width, height) VALUES (\"%@\" , \"\", \"%lld\", \"%d\", \"%d\")", url, ((long long int) [now timeIntervalSince1970]), imageWidth, imageHeight];
		
		const char *insert_stmt = [insertSQL UTF8String];
		
		sqlite3_prepare_v2(cacheDB, insert_stmt, -1, &statement, NULL);
		if (sqlite3_step(statement) == SQLITE_DONE)
		{
			sqlite3_int64 newrow = sqlite3_last_insert_rowid(cacheDB);
			NSString* filename = [NSString stringWithFormat:@"%lld.dat",newrow];
			
			NSString *updateSQL = [NSString stringWithFormat:@"UPDATE CACHE SET filename = \"%@\" WHERE ID = %lld",filename,newrow];
			
			const char *update_stmt = [updateSQL UTF8String];
			
			sqlite3_prepare_v2(cacheDB, update_stmt, -1, &statement2, NULL);
			if (sqlite3_step(statement2) == SQLITE_DONE)
			{
				//DLog(@"INSERTED OK!!!");
				filename = [cacheFilesPath stringByAppendingPathComponent:filename];
				//DLog(@"filename %@",filename);
				NSError* error = nil;
				if ([imageData writeToFile:filename options:0 error:&error]) {
					//DLog(@"file written");
				} else {
					DLog(@"file ERROR! %@",[error localizedDescription]);
				}
			} else {
				ALog(@"Failed to add cache file");
			}
			sqlite3_finalize(statement2);
		} else {
			const char * msg = sqlite3_errmsg(cacheDB);
			ALog(@"Failed to add cache, %s",msg);
			
		}
		sqlite3_finalize(statement);
		sqlite3_close(cacheDB);
	}
}

- (BOOL)checkIsExpired:(NSString*)modifiedtime
{
    NSTimeInterval i = [modifiedtime doubleValue] + cacheFileExpireTimeSeconds;
    NSDate* timestamp = [NSDate dateWithTimeIntervalSince1970:i];
    return ([timestamp timeIntervalSinceNow]<0.0 && !isOffLineMode);
}

- (NSString*) getLocalCacheFilePath
{
    return cacheFilesPath;
}

- (NSURL*) getLocalCacheFileBaseUrl
{
    return [NSURL fileURLWithPath:cacheFilesPath];
}

-(NSString *)getLocalCacheFileNameForUrl:(NSURL *)url
{
    return [self getLocalCacheFileNameForUrl:url size:nil initiateDownload:YES];
}

-(NSString *)getLocalCacheFileNameForUrl:(NSURL *)url size:(CGSize*)out_size 
{
    return [self getLocalCacheFileNameForUrl:url size:out_size initiateDownload:YES];
}

-(NSString *)getLocalCacheFileNameForUrl:(NSURL *)url size:(CGSize*)out_size initiateDownload:(BOOL)do_download
{
    const char *dbpath = [databasePath UTF8String];
	sqlite3_stmt    *statement;
	
	sqlite_int64 row_id = -1;
	bool remove_entry = false;
    
    NSString* result_filename = nil;
	
	if (sqlite3_open(dbpath, &cacheDB) == SQLITE_OK)
	{
		NSString *querySQL = [NSString stringWithFormat: @"SELECT filename, LASTMODIFIED, id, width, height FROM cache WHERE url=\"%@\"", url];
		
		const char *query_stmt = [querySQL UTF8String];
		
		if (sqlite3_prepare_v2(cacheDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
                const char* file_c_str = (const char *) sqlite3_column_text(statement, 0);
                if (file_c_str==nil) {
                    remove_entry=true;
                } else {
                    NSString *filename = [[NSString alloc] initWithUTF8String:file_c_str];
                    //NSString *filename_fullpath = [cacheFilesPath stringByAppendingPathComponent:filename];
                    
                    NSString *modifiedtime = [[NSString alloc] initWithUTF8String:((const char *) sqlite3_column_text(statement, 1) ? : "")];
                    
                    row_id = sqlite3_column_int64(statement, 2);
                    
                    if ([self checkIsExpired:modifiedtime]) {
                        DLog(@"cache expired");
                        remove_entry=true;
                    } else {
                        result_filename = filename;
                        
                        if (out_size!=nil) {
                            const char* imgwidth_c_str = (const char *) sqlite3_column_text(statement, 3);
                            const char* imgheight_c_str = (const char *) sqlite3_column_text(statement, 4);
                            
                            if (imgwidth_c_str!=nil && imgheight_c_str!=nil) {
                                out_size->width = atoi(imgwidth_c_str);
                                out_size->height = atoi(imgheight_c_str);
                                
                            }
                        }
                    }
                }
            }
            sqlite3_finalize(statement);
        }
        if (remove_entry && row_id!=-1) {
            [self removeEntry:row_id];
        }

        if (do_download) {
            [self loadUrl:url delegate:nil doDownloadOnly:YES DBAlreadyOpen:YES];
        }

        sqlite3_close(cacheDB);
    }
       //commence download if not found in cache
    return result_filename;
}
 

- (UrlCacheEntry*) loadUrl:(NSURL*)url delegate:(id<ImageLoadedNotificationDelegate>)delegate 
{
    return [self loadUrl:url delegate:delegate doDownloadOnly:NO DBAlreadyOpen:NO];
}

- (UrlCacheEntry*) loadUrl:(NSURL*)url delegate:(id<ImageLoadedNotificationDelegate>)delegate doDownloadOnly:(BOOL)download_only DBAlreadyOpen:(BOOL)db_already_open
{
	sqlite3_stmt    *statement;
	
	sqlite_int64 row_id = -1;
	bool remove_entry = false;
	bool download_url = false;

	bool openresult = true;
    if (!db_already_open) {
        openresult = [self openCacheDb];
    }
    
	if (openresult)
	{
		NSString *querySQL = [NSString stringWithFormat: @"SELECT filename, LASTMODIFIED, id FROM cache WHERE url=\"%@\"", url];
		
		const char *query_stmt = [querySQL UTF8String];
		int sqlerror=0;
		if ((sqlerror=sqlite3_prepare_v2(cacheDB, query_stmt, -1, &statement, NULL)) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
                const char* file_c_str = (const char *) sqlite3_column_text(statement, 0);
                if (file_c_str==nil) {
                    remove_entry=true;
                    download_url=true;
                } else {
                    NSString *filename = [[NSString alloc] initWithUTF8String:file_c_str];
                    NSString *filename_fullpath = [cacheFilesPath stringByAppendingPathComponent:filename];
                    
                    NSString *modifiedtime = [[NSString alloc] initWithUTF8String:((const char *) sqlite3_column_text(statement, 1) ? : "")];
                                        
                    row_id = sqlite3_column_int64(statement, 2);
                    
                    if ([self checkIsExpired:modifiedtime]) {
                        DLog(@"cache expired");
                        remove_entry=true;
                        download_url=true;
                    } else {
                        NSFileManager *filemgr = [[NSFileManager alloc] init];
                        if ([filemgr fileExistsAtPath: filename_fullpath ] == YES) {
                            DLog(@"cache hit");
                            if (!download_only) {
                                /* old (leaky?) code
                                NSData* imagedata = [[NSData alloc] initWithContentsOfFile:filename_fullpath];
                                if (imagedata!=nil && [imagedata length]>0) {
                                    UIImage* newimage = [[UIImage alloc] initWithData:imagedata];
                                    if (newimage!=nil) {
                                        [delegate imageLoaded:newimage];
                                    } else {
                                        DLog(@"error loading image from disk cache");
                                        remove_entry=true;
                                        download_url=true;
                                    }
                                 } else {
                                 */
                                //new code: load directly into a UIImage object
                                UIImage* newimage = [[UIImage alloc] initWithContentsOfFile:filename_fullpath];
                                if (newimage!=nil) {
                                    [delegate imageLoaded:newimage];
                                } else {
                                    DLog(@"error loading image data from cache");
                                    remove_entry=true;
                                    download_url=true;
                                }
                                
                            } else {
                                [delegate imageLoaded:nil]; //callback with nil since we don't need the image right now
                            }
                            
                        } else {
                            DLog(@"cache db hit but file missing");
                            remove_entry=true;
                            download_url=true;
                        }
                    } 
                }

			} else {
				DLog(@"cache miss");
				download_url=true;
			}
			sqlite3_finalize(statement);
		} else {
            ALog(@"SQLite error %d",sqlerror);
        }

        if (remove_entry && row_id!=-1) {
            [self removeEntry:row_id];
        }
        if (!db_already_open) {
            [self closeCacheDb];
        }
	}
	
	UrlCacheEntry* newentry = nil;
	
	if (download_url) {
		newentry = [UrlCacheEntry new];
		[newentry loadImageFromURL:url delegate:delegate];
	}
	return newentry;
	
}

- (void) removeCacheFile:(NSString*) filename
{
	NSString* filename_fullpath = [cacheFilesPath stringByAppendingPathComponent:filename];
	NSFileManager *filemgr = [[NSFileManager alloc] init];
	
	if ([filemgr fileExistsAtPath: filename_fullpath ] == YES) {
		if (![filemgr removeItemAtPath:filename_fullpath error:nil]) {
			DLog(@"problem removing file %@", filename_fullpath);
		}
	} else {
		DLog(@"file wasn't there (already removed): %@",filename);
	}
		
}


- (void) removeEntry:(sqlite_int64)row_id 
{
	sqlite3_stmt    *statement;
	
	//if (sqlite3_open(dbpath, &cacheDB) == SQLITE_OK)
	//{
		NSString *querySQL = [NSString stringWithFormat: @"SELECT filename, url FROM cache WHERE id=%lld", row_id];
		
		const char *query_stmt = [querySQL UTF8String];
		
		if (sqlite3_prepare_v2(cacheDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				//NSString *url = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];
				const char* url_cstr = (const char *) sqlite3_column_text(statement, 1);
				
				sqlite3_stmt    *statement2;
				NSString *updateSQL;
				if (url_cstr!=nil && url_cstr[0]!='\0') {
					//make sure to delete all rows with this url
					updateSQL = [NSString stringWithFormat:@"DELETE FROM cache WHERE url=\"%s\"",url_cstr];
				} else {
					updateSQL = [NSString stringWithFormat:@"DELETE FROM cache WHERE id=%lld",row_id];
				}
				
				const char *update_stmt = [updateSQL UTF8String];
				
				sqlite3_prepare_v2(cacheDB, update_stmt, -1, &statement2, NULL);
				if (sqlite3_step(statement2) == SQLITE_DONE)
				{
					DLog("successfully removed url from cache db");
				}	

                const char* file_cstr = (const char *) sqlite3_column_text(statement, 0);
                if (file_cstr!=nil) {
                    NSString *filename = [[NSString alloc] initWithUTF8String:file_cstr];
                    [self removeCacheFile:filename];
                }
				
				
				//[url release];
			} else {
				DLog(@"can't remove entry from db");
			}
			sqlite3_finalize(statement);
		}
		//sqlite3_close(cacheDB);
	//}
	
}

- (BOOL)checkFilenameInDB:(NSString*)filename
{
	sqlite3_stmt    *statement;
	
	BOOL result = FALSE;
	
		NSString *querySQL = [NSString stringWithFormat: @"SELECT COUNT(*) FROM cache WHERE filename=\"%@\"", filename];
		
		const char *query_stmt = [querySQL UTF8String];
		
		if (sqlite3_prepare_v2(cacheDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				result = (sqlite3_column_int(statement, 0) > 0);
				
			}
			sqlite3_finalize(statement);
		}
	
	return result;
}

- (void)checkConsistency
{
    [self checkConsistencyWithProgressDelegate:nil];
}

- (void)checkConsistencyWithProgressDelegate:(id<ProgressDisplayDelegate>)_delegate
{
	DLog(@"checking DB consistency...");
	
	NSFileManager *filemgr = [[NSFileManager alloc] init];

    int num_files=0;

    if (_delegate!=nil && [_delegate respondsToSelector:@selector(progressText:)]) {
        [_delegate progressText:@"Preparing cache..."];
    }
    
	NSError* error;
	NSArray* files = [filemgr contentsOfDirectoryAtPath:cacheFilesPath error:&error];
	NSMutableArray* files_to_remove = [NSMutableArray new];
	
    if ([self openCacheDb]) {
        
		const char *query_stmt4 = "CREATE INDEX IF NOT EXISTS fileidx ON cache(filename)";
        sqlite3_stmt    *statement4;

        int result=0;
		if ((result=sqlite3_prepare_v2(cacheDB, query_stmt4, -1, &statement4, NULL)) == SQLITE_OK)
		{
			result = sqlite3_step(statement4);
            DLog(@"result %d",result);
			sqlite3_finalize(statement4);
		}
        
        const char *query_stmt5 = "CREATE INDEX IF NOT EXISTS urlidx ON cache(url);";
        sqlite3_stmt    *statement5;
        
		if ((result=sqlite3_prepare_v2(cacheDB, query_stmt5, -1, &statement5, NULL)) == SQLITE_OK)
		{
			result = sqlite3_step(statement5);
            DLog(@"result %d",result);
			sqlite3_finalize(statement5);
		}
        
        //check that all files are ref'd from db
        for (NSString* fname in files) {
            BOOL is_in_db = [self checkFilenameInDB:fname];
            //DLog(@"filename %@ is in db: %d",fname, is_in_db);
            
            if (!is_in_db) {
                [files_to_remove addObject:fname];
            }
            num_files++;
            if (_delegate!=nil && num_files%100==0) {
                if ([_delegate respondsToSelector:@selector(progress:)]) {
                    [_delegate progress:((float)num_files/((float)[files count]))];
                    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
                }
            }
        }
        [self closeCacheDb];
    }
	
	//remove orphan files
	for (NSString* fname2 in files_to_remove) {
		DLog(@"removing file %@ from cache",fname2);
		[self removeCacheFile:fname2];
	}
	
	
	//check that DB only references files that exist and which aren't expired
	sqlite3_stmt    *statement;
	NSMutableArray* entries_to_remove = [NSMutableArray new];
    int sqlrow=0;
	
	if ([self openCacheDb])
	{
		NSString *querySQL = [NSString stringWithFormat: @"SELECT filename, LASTMODIFIED, id FROM cache"];
		
		const char *query_stmt = [querySQL UTF8String];
		
		if (sqlite3_prepare_v2(cacheDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				bool do_remove = false;
				NSString *modifiedtime = [[NSString alloc] initWithUTF8String:((const char *) sqlite3_column_text(statement, 1) ? : "")];
				NSTimeInterval i = [modifiedtime doubleValue] + cacheFileExpireTimeSeconds;
				NSDate* timestamp = [NSDate dateWithTimeIntervalSince1970:i];
				
				if ([timestamp timeIntervalSinceNow]<0.0) {
					DLog(@"cache expired");
					do_remove = true;
				} else {
                    const unsigned char *filename_cstr = sqlite3_column_text(statement, 0);
                    if (filename_cstr!=nil) {
                        NSString* filename = [[NSString alloc] initWithUTF8String:((const char *) filename_cstr ? : "")];
                        NSString *filename_fullpath = [cacheFilesPath stringByAppendingPathComponent:filename];
                        
                        //hent filnavn og check om det er i pathen
                        if ([filemgr fileExistsAtPath: filename_fullpath ] == NO)  {
                            DLog(@"db entry's file missing");
                            do_remove = true;
                        }
                    } else {
                        do_remove=true;
                    }
				}

				
				if (do_remove) {
					[entries_to_remove addObject:[NSNumber numberWithLongLong:sqlite3_column_int64(statement, 2)]];
					
				}
                sqlrow++;
                if (_delegate!=nil && sqlrow%100==0) {
                    if ([_delegate respondsToSelector:@selector(progress:)]) {
                        [_delegate progress:((float)sqlrow/((float)num_files))];
                        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
                    }
                }

			}
			
			sqlite3_finalize(statement);
		} else {
			DLog(@"sqlite reported error");
		}
        
        //remove orphan entries
        for (NSNumber* num in entries_to_remove) {
            DLog(@"removing entry %lld from cache",[num longLongValue]);
            [self removeEntry:[num longLongValue]];
        }

		[self closeCacheDb];
	}
	
	
	
	
	DLog(@"checking DB consistency done.");
}

- (void)clearEntireCacheDatabase
{
	NSFileManager *filemgr = [[NSFileManager alloc] init];
	NSError* error;
	[filemgr removeItemAtPath:databasePath error:&error];
	
	[self setupDB];
	[self checkConsistency]; //effectively remove all files
}


@end
